This script demonstrates how to use the moveVis R package to create an animation of coyote tracking info. (Scroll to the bottom to see the final result!)
First load the packages we’ll need:
library(moveVis)
library(move)
library(readr)
library(dplyr)
library(lubridate)
Grab the CSV file:
cous12_fn <- "cous12.csv"; file.exists(cous12_fn)
[1] TRUE
Uncomment the following line if you want to preview the CSV file in a View pane. This can help you see the column names and their data types.
# readr::read_csv(cous12_fn) |> head() |> View()
Import the CSV file as a tibble. We only need 4 columns:
cous12_tbl <- readr::read_csv(cous12_fn, col_select = c(GpsDescription, GPSTime, lon, lat))
Rows: 1516 Columns: 4── Column specification ────────────────────────────────────────────────────────────────────
Delimiter: ","
chr (2): GpsDescription, GPSTime
dbl (2): lat, lon
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Inspect what we got:
head(cous12_tbl)
Convert the GPSTime column (which is a character object) to a POSIXct column. For this we’ll pick a function from lubridate that matches the date formatting we got.
cous12_dt_tbl <- cous12_tbl |>
mutate(gps_dt = dmy_hm(GPSTime))
head(cous12_dt_tbl)
How many rows and columns?
dim(cous12_dt_tbl)
[1] 1516 5
First we have to deal with duplicate timestamps, which will cause a problem when we convert the data frame to a move object.
How many duplicate time stamps are there?
cous12_dt_tbl |>
group_by(gps_dt) |>
summarize(num_pts = n()) |>
filter(num_pts > 1) |>
dim()
[1] 58 2
This tells us there are 58 time stamps that have more than one row. Perhaps there were two locations within the same minute (the GPS time stamps don’t include seconds). Duplicate locations are not that uncommon in many animal tracking datasets. If you want to explore further you could open the CSV file in Excel and examine them, but we will just delete them.
An easy way to get rid of duplicates is by grouping:
cous12_dt_nodups_tbl <- cous12_dt_tbl |>
group_by(gps_dt) |>
summarize(num_pts = n(),
GpsDescription = first(GpsDescription),
lon = first(lon),
lat = first(lat)) |>
ungroup()
head(cous12_dt_nodups_tbl)
Compare the number of rows before and after removing duplicate timestamps:
dim(cous12_dt_tbl)
[1] 1516 5
dim(cous12_dt_nodups_tbl)
[1] 1458 5
cous12_mov <- moveVis::df2move(cous12_dt_nodups_tbl,
proj = "+init=epsg:4326 +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0",
x = "lon", y = "lat", time = "gps_dt",
track_id = "GpsDescription")
We can ignore the warning about the datum. moveVis is still using a rgdal which is the underlying problem.
See what we got:
cous12_mov
class : Move
features : 1458
extent : -117.8791, -117.6741, 33.52707, 34.01647 (xmin, xmax, ymin, ymax)
crs : +proj=longlat +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +no_defs
variables : 3
names : x, y, time
min values : -117.87915, 33.52706667, 1670955420
max values : -117.6741, 34.01646667, 1672660800
timestamps : 2022-12-13 18:17:00 ... 2023-01-02 12:00:00 Time difference of 20 days (start ... end, duration)
sensors : unknown
indiv. data :
indiv. value:
date created: 2022-12-26 19:27:02
dim(cous12_mov)
[1] 1458 3
To make the animation, we’re going to have to select the number of minutes/seconds represented by each frame. We’ll need this when we use the moveVis function that will ‘align’ the dataset to this sampling interval, interpolating locations as needed. This dataset only has the locations for one coyote, but if it contained multiple coyotes their locations would all be interpolated to the same frame interval.
There are always a few gaps in movement datasets, but in general it makes sense to use the dominant sampling interval:
timeLag(cous12_mov, unit = "secs") |> table()
60 120 180 240 360 420 480 540 600 660 720 780 840 900 960
3 1 1 1 3 4 1 9 7 9 10 4 1 1061 297
1020 1080 1140 1440 1500 1560 1620 1800 2040 4020 6540 20820 21360 21600 21840
6 3 1 2 6 4 1 2 1 1 1 1 1 12 1
22380 43200
1 1
timeLag(cous12_mov, unit = "secs") |> median()
[1] 900
900 seconds is 15 minutes. Align all the samples to be 900s apart:
cous12_900s_mov <- align_move(cous12_mov, res = 900, unit = "secs")
dim(cous12_900s_mov)
[1] 1895 3
moveVis allows you to provide your own basemap image (eg., a GeoTIFF), or you can grab one on-the-fly from Open Street Map or MapBox. For urban coyotes, a satellite image would look nice (see you can see the parks and greenways), but for expediency we’ll use OSM:
get_maptypes(map_service = 'osm')
[1] "streets" "streets_de" "streets_fr" "humanitarian" "topographic"
[6] "roads" "hydda" "hydda_base" "hike" "grayscale"
[11] "no_labels" "watercolor" "toner" "toner_bg" "toner_lite"
[16] "terrain" "terrain_bg" "mtb"
Creating frames will go a bit faster if we set up parallel processing:
use_multicore(n_cores = NULL, verbose = TRUE)
Number of cores set to be used by moveVis: 11 out of 12
We can also tell it to save the frames to disk (to save on memory)
use_disk(
frames_to_disk = TRUE,
dir_frames = paste0(tempdir(), "/moveVis"),
n_memory_frames = 100,
verbose = TRUE
)
Disk usage for creating frames enabled.
Directory: 'C:\Users\Andy\AppData\Local\Temp\RtmpotXbwy/moveVis'
Maximum number of frames which will be hold in memory: 100
We create the frames with frames_spatial(). There are
lots of arguments in frames_spatial(), and extra functions
you can tack on to create the frames. Getting the right look is going to
be a matter and trial and error. making frames is pretty fast, but you
can use subset_move() to work on just a subset of the
locations.
One thing to note is that we don’t specify the frame size or resolution when we’re constructing the frames. Under the hood, each frame is an unrendered ggplot object. We’ll specify the output resolution in the last step when we ‘stitch’ the frames into an animation file.
On my machine generating 1895 frames took ~4 minutes:
frames <- frames_spatial(cous12_900s_mov, path_colours = c("red"),
map_service = "osm", map_type = c("streets", "watercolor")[1], alpha = 0.5) |>
add_labels(x = "Long", y = "Lat") |>
add_northarrow() |>
add_scalebar() |>
add_timestamps(type = "label") %>% add_progress()
Checking temporal alignment...
Processing movement data...
Approximated animation duration: ≈ 75.8s (~1.26 minutes) at 25 fps for 1895 frames
Warning: GDAL Message 1: +init=epsg:XXXX syntax is deprecated. It might return a CRS with a non-EPSG compliant axis order.
Retrieving and compositing basemap imagery...
| | 0 % ~calculating
|======== | 10% ~01s
|=============== | 20% ~01s
|====================== | 30% ~01s
|============================= | 40% ~00s
|==================================== | 50% ~00s
|=========================================== | 60% ~00s
|================================================== | 70% ~00s
|========================================================= | 80% ~00s
|================================================================ | 90% ~00s
|=======================================================================| 100% elapsed=01s
Error in x$.self$finalize() : attempt to apply non-function
Error in (function (x) : attempt to apply non-function
Assigning raster maps to frames...
Creating frames...
| | 0 % ~calculating
|= | 1 % ~56s
|== | 2 % ~56s
|=== | 3 % ~55s
|=== | 4 % ~55s
|==== | 5 % ~54s
|===== | 6 % ~53s
|===== | 7 % ~53s
|====== | 8 % ~52s
|======= | 9 % ~52s
|======== | 10% ~51s
|======== | 11% ~51s
|========= | 12% ~50s
|========== | 13% ~50s
|========== | 14% ~50s
|=========== | 15% ~49s
|============ | 16% ~49s
|============= | 17% ~48s
|============= | 18% ~48s
|============== | 19% ~47s
|=============== | 20% ~48s
|=============== | 21% ~47s
|================ | 22% ~47s
|================= | 23% ~46s
|================== | 24% ~45s
|================== | 25% ~45s
|=================== | 26% ~44s
|==================== | 27% ~44s
|==================== | 28% ~43s
|===================== | 29% ~42s
|====================== | 30% ~42s
|======================= | 31% ~41s
|======================= | 32% ~40s
|======================== | 33% ~40s
|========================= | 34% ~39s
|========================= | 35% ~39s
|========================== | 36% ~38s
|=========================== | 37% ~37s
|=========================== | 38% ~37s
|============================ | 39% ~36s
|============================= | 40% ~36s
|============================== | 41% ~35s
|============================== | 42% ~34s
|=============================== | 43% ~34s
|================================ | 44% ~33s
|================================ | 45% ~33s
|================================= | 46% ~32s
|================================== | 47% ~31s
|=================================== | 48% ~31s
|=================================== | 49% ~30s
|==================================== | 50% ~30s
|===================================== | 51% ~29s
|===================================== | 52% ~29s
|====================================== | 53% ~28s
|======================================= | 54% ~27s
|======================================== | 55% ~27s
|======================================== | 56% ~26s
|========================================= | 57% ~26s
|========================================== | 58% ~25s
|========================================== | 59% ~25s
|=========================================== | 60% ~24s
|============================================ | 61% ~23s
|============================================= | 62% ~23s
|============================================= | 63% ~22s
|============================================== | 64% ~22s
|=============================================== | 65% ~21s
|=============================================== | 66% ~20s
|================================================ | 67% ~20s
|================================================= | 68% ~19s
|================================================= | 69% ~19s
|================================================== | 70% ~18s
|=================================================== | 71% ~17s
|==================================================== | 72% ~17s
|==================================================== | 73% ~16s
|===================================================== | 74% ~16s
|====================================================== | 75% ~15s
|====================================================== | 76% ~14s
|======================================================= | 77% ~14s
|======================================================== | 78% ~13s
|========================================================= | 79% ~13s
|========================================================= | 80% ~12s
|========================================================== | 81% ~11s
|=========================================================== | 82% ~11s
|=========================================================== | 83% ~10s
|============================================================ | 84% ~10s
|============================================================= | 85% ~09s
|============================================================== | 86% ~09s
|============================================================== | 87% ~08s
|=============================================================== | 88% ~07s
|================================================================ | 89% ~07s
|================================================================ | 90% ~06s
|================================================================= | 91% ~05s
|================================================================== | 92% ~05s
|=================================================================== | 93% ~04s
|=================================================================== | 94% ~04s
|==================================================================== | 95% ~03s
|===================================================================== | 96% ~02s
|===================================================================== | 97% ~02s
|====================================================================== | 98% ~01s
|=======================================================================| 99% ~01s
|=======================================================================| 100% elapsed=01m 01s
frames is a list. Let’s see how many frames it made:
length(frames)
[1] 1895
Each element of frames is a ggplot object:
class(frames[[100]])
[1] "gg" "ggplot"
Let’s preview one of them:
The final step is to ‘stitch’ all the frames together into an animation. Under the hood, moveVis uses ffmpeg. There are a number of output file formats supported. I would recommend animated gifs (which work reliably in PowerPoint, Google Slides, and HTML pages) or mp4.
This is probably the longest step in the process. On my laptop, this step took ~30 minutes to make a GIF, and 27 minutes to make a mp4. You can’t really speed it up because video encoding in general can not be parallelized.
animate_frames(frames, out_file = "cous12_v1.gif",
fps = 25,
width = 700,
height = 700,
res = 100,
overwrite = FALSE)
Error: Defined output file already exists and overwriting is disabled.
What did we get?